package pl.koszolko.documentclustering.cluster;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.apache.commons.math3.stat.StatUtils;
import org.apache.log4j.Logger;

import pl.koszolko.documentclustering.cluster.document.DocumentForClustering;
import pl.koszolko.documentclustering.cluster.quality.FQualityHelper;
import pl.koszolko.documentclustering.cluster.quality.IndexRandQualityHelper;
import pl.koszolko.documentclustering.cluster.quality.PurityQualityHelper;

public class ClusteringAlgorithmAgglomerative implements ClusteringAlgorithm {
	private static Logger logger = Logger.getLogger(ClusteringAlgorithmAgglomerative.class);
	private static Logger loggerQuality = Logger.getLogger("wartosci_miar");
	
	private static IndexRandQualityHelper indexRandQualityHelper = new IndexRandQualityHelper();
	private static PurityQualityHelper purityQualityHelper = new PurityQualityHelper();
	private static FQualityHelper fQualityHelper = new FQualityHelper();
	
	/* (non-Javadoc)
	 * @see pl.koszolko.documentclustering.cluster.ClusteringAlgorithm#hierarchicalClustering(java.util.List)
	 */
	@Override
	public Dendrogram hierarchicalClustering(List<DocumentForClustering> documents) {
		Dendrogram dendrogram = new Dendrogram();
		List<Cluster> clusters = createInitialClusters(documents);
		dendrogram.setInitialClusters(clusters);
		ExecutorService executor = Executors.newFixedThreadPool(40);
		
		//oblicznie liczby koncowej klastrow
		//int finalNumberOfCluster = (int) Math.sqrt(clusters.size()/2);
		int finalNumberOfCluster =7;
		
		//lista miar podobienstwa
		List<Double> similarityList = new ArrayList<Double>();
		
		double similarity = 1;
		clusters = dendrogram.getClustersToMarge();
		
		while(clusters.size() > 1 && clusters.size() >finalNumberOfCluster) {
			logger.info("Klastrow do polaczenia jest " + clusters.size());
			similarity = margeClusters(clusters, dendrogram, executor);
			similarityList.add(similarity);
			if(similarity<=0) {
				break;//nie ma sensu grupowac
			}
			clusters = dendrogram.getClustersToMarge();
			logger.info("Jakosc purity = " + purityQualityHelper.measureQuality(clusters));
			logger.info("Jakosc index randa = " + indexRandQualityHelper.measureQuality(clusters));
			logger.info("Jakosc wskaznik F = " + fQualityHelper.measureQuality(clusters));
		}
		
		double[] similarityArray = createSimilarityArray(similarityList);
		
		double mean = StatUtils.mean(similarityArray);
		double median = StatUtils.percentile(similarityArray, 50);
		double variance = StatUtils.variance(similarityArray);
		
		logger.info("Mean  = " + mean);
		logger.info("Median = " + median);
		logger.info("Variance = " + variance);
		logger.info("Stop value = " + (median-variance));
		logger.info("Ostateczna jakosc purity = " + purityQualityHelper.measureQuality(clusters));
		logger.info("Ostateczna jakosc index randa = " + indexRandQualityHelper.measureQuality(clusters));
		logger.info("Ostateczna jakosc wskaznika F = " + fQualityHelper.measureQuality(clusters));
		
		int i=0;
		for(Double s : similarityList) {
			loggerQuality.info(++i + " " + s);
		}
		
		executor.shutdown();
		
		return dendrogram;
	}

	private double[] createSimilarityArray(List<Double> similarityList) {
		double[] similarityArray = new double[similarityList.size()];
		for(int i=0 ; i<similarityList.size() ; ++i) {
			similarityArray[i] = similarityList.get(i);
		}
		return similarityArray;
	}

	private List<Cluster> createInitialClusters(List<DocumentForClustering> documents) {
		List<Cluster> clusters = new ArrayList<Cluster>();

		for (DocumentForClustering d : documents) {
			Cluster cluster = new ClusterMinDistance();
			cluster.addDocument(d);
			clusters.add(cluster);
		}

		return clusters;
	}

	private double margeClusters(List<Cluster> clusters, Dendrogram dendrogram, ExecutorService executor) {
		logger.debug("Start margeClusters");
		
		Cluster cluster1 = new ClusterMinDistance(), cluster2 = new ClusterMinDistance();
		double max = 0, tmp;

		List<Future<Result>> results = new ArrayList<Future<Result>>();
		
		// znajdz klastry do polaczenia
		for(int i=0 ; i<clusters.size()-1 ; ++i) {
			for(int j=i+1 ; j<clusters.size() ; ++j) {
				//utworzenie zadania i dodanie do poli
				Callable<Result> task = new CountDistanceTask(clusters.get(i), clusters.get(j));
				Future<Result> result = executor.submit(task);
				results.add(result);
			}
		}

		//interpretacja wynikow
		for(Future<Result> result : results) { 
			try {
				Result r;
				r = result.get();
				logger.debug("Odleglosc miedzy klastrami wynosi = " + r.getDistance());
				tmp = r.getDistance();
				if(tmp >= max) {
					logger.debug("Sa to najbardziej podobne klastry, poprzednie = " + tmp);
					max = tmp;
					cluster1 = r.getC1();
					cluster2 = r.getC2();
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			} catch (ExecutionException e) {
				e.printStackTrace();
			}
		}
		
		logger.info("Najbardziej podobne klastry, wynik = " + max);
		
		// utworz nowy klaster
		Cluster newCluster = new ClusterMinDistance();
		newCluster.addDocuments(cluster1.getDocuments());
		newCluster.addDocuments(cluster2.getDocuments());

		// dodaj nowy klaster do dendrogramu
		dendrogram.addNewCluster(newCluster, cluster1, cluster2, max);
		
		logger.debug("End margeClusters");
		
		return max;
	}
	
	public class CountDistanceTask implements Callable<Result> {
		private Cluster c1;
		private Cluster c2;
		
		public CountDistanceTask(Cluster c1, Cluster c2) {
			this.c1 = c1;
			this.c2 = c2;
		}
		
		@Override
		public Result call() throws Exception {
			Result r = new Result();
			r.setC1(c1);
			r.setC2(c2);
			r.setDistance(c1.countDistance(c2));
			return r;
		}
	}
	
	public class Result {
		private Cluster c1;
		private Cluster c2;
		private Double distance;
		
		public Cluster getC1() {
			return c1;
		}
		public void setC1(Cluster c1) {
			this.c1 = c1;
		}
		public Cluster getC2() {
			return c2;
		}
		public void setC2(Cluster c2) {
			this.c2 = c2;
		}
		public Double getDistance() {
			return distance;
		}
		public void setDistance(Double distance) {
			this.distance = distance;
		}
	}
	
	private class MinDistanceKeyMap {
		private Long idDocument1;
		private Long idDocument2;
		
		public MinDistanceKeyMap(Long idDocument1, Long idDocument2) {
			this.idDocument1 = idDocument1;
			this.idDocument2 = idDocument2;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + getOuterType().hashCode();
			result = prime * result
					+ ((idDocument1 == null) ? 0 : idDocument1.hashCode());
			result = prime * result
					+ ((idDocument2 == null) ? 0 : idDocument2.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			MinDistanceKeyMap other = (MinDistanceKeyMap) obj;
			if (!getOuterType().equals(other.getOuterType()))
				return false;
			if (idDocument1 == null) {
				if (other.idDocument1 != null)
					return false;
			} else if (!idDocument1.equals(other.idDocument1))
				return false;
			if (idDocument2 == null) {
				if (other.idDocument2 != null)
					return false;
			} else if (!idDocument2.equals(other.idDocument2))
				return false;
			return true;
		}

		private ClusteringAlgorithmAgglomerative getOuterType() {
			return ClusteringAlgorithmAgglomerative.this;
		}
	}
}
